Repository navigation
fix(remote-chat): join a room stored as one row per member without dropping a line (#2804) - #2820
Conversation
|
@Birfy — review request for the current tip, once for this head. Tip: What it does: Checks at this tip: 16 success + 2 skipped ( FlyBot Test's advisory self-test at this tip passed (13/13 at the head, red-first Please post it as |
…opping a line (ThinkFlowLab#2804) A state written before ThinkFlowLab#2797 keeps one chat per member of the same room: every one of those rows carries the room key, so sync's gone check never fires and nothing joins them. mergeRoom() runs on the next sync, open or deliver and hangs each duplicate's Session off the oldest row as an earlier part — no message is copied, so every line reads back word for word and an assistant reply stays one. The merge also rebinds the chat's Session to the survivor's handle (otherwise the first line typed there is "Not sent"), moves an open question card up without its call id (otherwise nobody can answer it), and leaves a room with more than one open card unmerged for that round rather than dropping one.
|
@Birfy Review request for this head, once per tip. Tip: This tip is the re-entrancy fix on top of the advisory FAIL on Checks at this tip: 15 success + 2 skipped + 0 cancelled + 1 failure. The only failure is Scope: Please reply on this sha in the form:
|
…hinkFlowLab#2804) `sync` and `deliver` do not serialise each other, so a caller that arrived while the route select was pending still saw the duplicate rows, ran the whole merge a second time and appended the same boundaries and Sessions to the survivor — a line shown twice on disk after a restart. The rows go before that await, with everything the await needs taken into a local first; a later caller now finds one row and returns before writing anything. A test holds the route select and overlaps two deliveries, and a sync with a delivery: it fails on the previous tip with ["s1","s2","s3","s2"] and passes here.
|
Verdict: PASS at ffa6c2e Ran at this tip: Not run: the full Why this PASS:
Known display boundary, accepted as declared (not fixed here): an earlier part's This is a Verifier report, not merge authority. |
Fixes #2804
Owner: Dev-3
Reviewer: Reviewer
Written-by: Dev-3
Bot: Dev-3
Risk tier: T2
Base: 75781c1
Claim-Id: 92849141a93448c3bbc08e96eeddf4a1
What changed
A state written before #2797 holds one chat per member of the same published room. Every one of those
rows carries the room key, so
sync()'s gone check (remote-chat.js:304-305) reads each as "the room isstill published": none is marked gone, nothing joins them, the list shows the room up to eight times, and
each member's line lands in its own row (
open()/deliver()trychatOfHandlefirst,:254,:334).mergeRoom(key, plugin)joins them on the nextsync(),open()ordeliver(), before the handlelookup:
remote.handle, name, model route,hidden) and every other row'sSession becomes an earlier part of it (
bot.segments, the shapepartsOfcomposes atparts.js:299-308), with the newest Session current — so the thread reads in the order the rows weremade and no message is copied: every line reads back word for word and an assistant reply stays an
assistant reply;
chatOwners/pastOwnersare re-pointed at the survivor before the rows go (the Session ids are readbefore the chat's own is replaced), so
selfOf,earlierPageandchat-indexresolve every partwithout a reload;
remoteAwaiting,remoteContacts(one handle keeps the earlier first-seen time) andfriendLineAt(max) move with the chat instead of vanishing with the deleted rows.
A room with fewer than two rows returns
falsebefore the first write, so a room that was neverduplicated is not rewritten at all.
Re-entrancy. The merge writes and then awaits
selectSessionModel, andsyncanddeliverdo notserialise each other (
hooks.jspublishesremoteChats.syncwithout awaiting it while the inbox loopdelivers), so a caller that arrived in that await still saw the duplicate rows and ran the whole write a
second time — the same boundaries and Sessions appended to the survivor again, a line shown twice on disk
after a restart. The rows now go before the await, with the values the await needs taken into a local
first; a later caller finds one row and returns before it writes anything.
Two regressions the merge itself would create are fixed in this PR (#2804 acceptance 9 and 10): the
Session that becomes current was opened for another row's handle, and a turn runs on the Session's own
route while
forwardsends to the handle in it — left alone, the first line typed in the merged chat isNot sent; and a question card is answered on the chat's current Session, so a card left open in one ofthe rows became unanswerable. They stay here rather than in a follow-up because they are regressions the
merge itself creates, and shipping the merge without them would ship a broken merge on purpose.
Acceptance, as verified below: after the merge every line the user can read on that thread is still
there, word for word — all eight Sessions are read back through
partsOfas{ type, text }in creationorder and compared element by element, with
followupsempty (nothing was injected or replayed); and aroom with two or more open question cards is not merged that round (
false, zero writes, retried onthe next
sync/open/deliver) instead of deleting a card and leaving its wait hanging.Known display boundary
An earlier part's
endedAtis the next row'screatedAt. When the oldest row itself rotated later, thatpart's
endedAtcan be earlier than the part really started, so a divider can sit one boundary early. Notext is affected and nothing becomes unreadable; this is declared here, not fixed in this PR.
Verification at the tip
Tip SHA:
ffa6c2e57e5dc9b5566edf13f1e42e9772b0eee4(treedcc3aa659a2b0986da13e551193e045c175590c8,base
75781c182d483a8fe78a558a743267ccfe8fa1b8).Ran:
The re-entrancy case holds
selectSessionModelopen and overlaps twodeliver()calls, then async()with a
deliver(): it asserts one row, no Session twice amongsegments+ current, every written lineexactly once, and the total line count. On the previous tip the first
deliver's merge reaches the awaitwith the rows still in place, the second runs the whole write again, and the survivor ends at
["s1","s2","s3","s2"](plus its current Session) — the shape the advisory named.The ten red readings, verbatim:
the room is one row, not 8,the survivor's history lost or reordered a duplicate's line: ["line from member1"],member2's line went to d2, not the room's one row (d1), theverbatim reading stopping at
the room is one row, not 8,the earlier parts of a duplicate row were lost or reordered: [],a part ends at the wrong row: [],the senders seen in this room were lost: {"d2":{"@a":100,"@b":40},"d5":{"@a":50}},the chat's Session still runs the route of a deleted row: [],the open card is gone from the chat, andthe room did not join after one answer came back: ["d1","d2"].Neighbourhood, 11 files (this one,
remote-chat,remote-room,remote-approval-wait,roster,store,questions,questions-readonly,room-row-question,room-approvals-readonly,chat-index-writers):# tests 248 / # pass 248 / # fail 0.Full
coresuite:# tests 3214 / # pass 3211 / # fail 3. The three are machine reds and fail the sameat this base with the source face reverted:
bench-preflight.test.js:80and:88(mkdir: not foundinside the bench sandbox) and
thinking-row.test.js:214(the#1718box measurement).Not run: a browser against a real install — the merge is read at the Session layer, which the tests cover.
Generated artifacts (not smuggling)
node scripts/ds-bot-patches.mjs generatewrote only what this change owns:patches/ds-bot/files/src/host/remote-chat.js(the whole-file mirror of the edited source) andpatches/ds-bot/files/test/p689-remote-room-merge.test.js(the new test's mirror). No numbered patchchanged:
remote-chat.jsand the test are whole-file additions onmainat this base, andcanonicalagrees. Nothing else moved.
Files
Six files, all additions against this base (
+1098 / -0); nothing was replaced or reformatted, which iswhy the branch has no deletions even though the new commit moves a line.
New files (3)
core/test/p689-remote-room-merge.test.js— 429 lines, the 11 readings and 3 controls.patches/ds-bot/files/test/p689-remote-room-merge.test.js— 429 lines, thegeneratemirror of it.changelog.d/2804-remote-room-merge.fixed.md— 1 line, the user-visible change.Modified existing files (3), additions only
core/src/host/remote-chat.js— +119 / -0: one newmergeRoom()function (with its re-entrancyorder) plus its three call sites (
sync(),open(),deliver()). No old path was deleted orrewritten; the merge only runs when the rows it merges already exist.
patches/ds-bot/files/src/host/remote-chat.js— +119 / -0, thegeneratemirror of that file.docs/subsystems/core-host.md— +1 / -0, one sentence on the remote-chat page.The test's fixture: built inline, no shared helper to import. The file declares its own
ROOM,HOME,member()and its ownemptyState()/duplicates()builders. There is no importable fixture:roomHost(core/test/remote-chat.test.js:375) andmember(:391), used by the#2797group(
:393-:446), are local to that file, androomHostonly creates new rows throughcreateBot, so itcannot build the pre-existing one-row-per-member state this test needs. The new file follows that file's
shape and names, not its code.
Mirrors.
patches/ds-bot/files/**isnode scripts/ds-bot-patches.mjs generateoutput and isbyte-identical to the
core/files above;ds-bot-patches checkandcanonicalprove it, so there isnothing to review twice.
Checkpoint
{ "pr": 2820, "claim": "Dev-3", "issue": 2804, "summary": "A room a state written before #2797 holds as one chat per member is joined on the next sync, open or deliver: the oldest row survives, every other row's Session becomes an earlier part, and no message is copied. The rows go before the merge's one await, so a caller arriving there (sync and deliver overlap) finds one row and writes nothing; otherwise the same parts were appended twice. The chat's Session is rebound to the surviving handle, an open card moves up, and the awaiting and contact records move with it. The test file is 14 cases: 13 pass / 1 fail on the previous tip, 14/14 here.", "files": ["core/src/host/remote-chat.js", "core/test/p689-remote-room-merge.test.js", "docs/subsystems/core-host.md"], "checks": { "ran": "node --test (14/14 twice; re-entrancy red 13 pass / 1 fail at e0050974; feature red 3 pass / 10 fail at base, twice), 11-file neighbourhood 248/248, ds-bot-patches check+canonical, lint+arch-docs, full core 3214/3211", "result": "green", "at": "ffa6c2e57e5dc9b5566edf13f1e42e9772b0eee4" }, "next": "none — ready for review" }